/*
 * Copyright (c) 2014 Hai Bison
 *
 * See the file LICENSE at the root directory of this project for copying
 * permission.
 */

package haibison.simpleprovider.services;

import static haibison.simpleprovider.BuildConfig.DEBUG;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import android.Manifest.permission;
import android.app.PendingIntent;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.text.TextUtils;
import android.util.Log;

/**
 * CPO stands for Content Provider Operations. This service executes
 * {@link ContentProviderOperation}'s and sends back results to your
 * listener/commander.
 * <p/>
 * <h1>Notes</h1>
 * <ul>
 * <li>This service extends {@link BaseIntentService}. So to have it work
 * properly in background, you have to use {@link permission#WAKE_LOCK}.</li>
 * <li>You can use {@link #EXTRA_RESULT_RECEIVER} and/or
 * {@link #EXTRA_PENDING_INTENT_RESULT} to receive results.</li>
 * </ul>
 * 
 * @author Hai Bison
 *
 */
public class CPOExecutor extends BaseIntentService {

    private static final String CLASSNAME = CPOExecutor.class.getName();

    /**
     * Use this action to execute operations. You have to provide
     * {@link #EXTRA_AUTHORITY} and {@link #EXTRA_OPERATIONS}.
     */
    public static final String ACTION_EXECUTE_OPERATIONS = CLASSNAME
            + ".EXECUTE_OPERATIONS";

    /**
     * This extra holds the session ID, must be positive.
     * <p/>
     * Type: input, Integer, required.
     *
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final String EXTRA_SESSION_ID = CLASSNAME + ".SESSION_ID";

    /**
     * This extra holds the authority.
     * <p/>
     * Type: input, String, required.
     *
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final String EXTRA_AUTHORITY = CLASSNAME + ".AUTHORITY";

    /**
     * This extra holds an {@link ArrayList} of {@link ContentProviderOperation}
     * .
     * <p/>
     * Type: input, {@link ArrayList} of {@link ContentProviderOperation},
     * required.
     *
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final String EXTRA_OPERATIONS = CLASSNAME + ".OPERATIONS";

    /**
     * This extra holds a {@link ResultReceiver}.
     * <p/>
     * Type: input, {@link ResultReceiver}, optional.
     * 
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final String EXTRA_RESULT_RECEIVER = CLASSNAME
            + ".RESULT_RECEIVER";

    /**
     * This extra holds a {@link PendingIntent}.
     * <p/>
     * Type: input, {@link PendingIntent}, optional.
     * 
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final String EXTRA_PENDING_INTENT_RESULT = CLASSNAME
            + ".PENDING_INTENT_RESULT";

    /**
     * Result code is OK.
     * 
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final int RESULT_OK = 0;

    /**
     * Result code is error.
     * 
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final int RESULT_ERROR = 1;

    /**
     * This extra holds an array of {@link ContentProviderResult}.
     * <p/>
     * Type: output, array of {@link ContentProviderResult}.
     * 
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final String EXTRA_CONTENT_PROVIDER_RESULTS = CLASSNAME
            + ".CONTENT_PROVIDER_RESULTS";

    /**
     * This extra holds the throwable.
     * <p/>
     * Type: output, {@link Throwable}.
     * 
     * @see #ACTION_EXECUTE_OPERATIONS
     */
    public static final String EXTRA_THROWABLE = CLASSNAME + ".THROWABLE";

    /**
     * Intent builder.
     * 
     * @author Hai Bison
     *
     */
    public static class IntentBuilder {

        private static final String[] REQUIRED_EXTRAS = { EXTRA_AUTHORITY,
                EXTRA_OPERATIONS, EXTRA_SESSION_ID };

        private final Intent mResult;

        /**
         * Creates new instance with default action
         * {@link CPOExecutor#ACTION_EXECUTE_OPERATIONS}.
         * 
         * @param context
         *            the context.
         */
        public IntentBuilder(Context context) {
            mResult = new Intent(ACTION_EXECUTE_OPERATIONS, null, context,
                    CPOExecutor.class);
        }// IntentBuilder()

        /**
         * Builds the intent.
         * 
         * @return new intent.
         * @throws IllegalArgumentException
         *             if one of required extras is missing.
         */
        public Intent build() {
            for (String extra : REQUIRED_EXTRAS) {
                if (!mResult.hasExtra(extra))
                    throw new IllegalArgumentException(
                            "Missing required extra: " + extra);
            }// for

            return mResult;
        }// build()

        /**
         * Sets {@link CPOExecutor#EXTRA_SESSION_ID}.
         * 
         * @param id
         *            session ID.
         * @return the instance of this builder, for chaining calls.
         */
        public IntentBuilder setSessionId(int id) {
            mResult.putExtra(EXTRA_SESSION_ID, id);
            return this;
        }// setSessionId()

        /**
         * Sets {@link CPOExecutor#EXTRA_AUTHORITY}.
         * 
         * @param authority
         *            authority.
         * @return the instance of this builder, for chaining calls.
         */
        public IntentBuilder setAuthority(String authority) {
            mResult.putExtra(EXTRA_AUTHORITY, authority);
            return this;
        }// setAuthority()

        /**
         * Adds operations to {@link CPOExecutor#EXTRA_OPERATIONS}.
         * 
         * @param operations
         *            operations.
         * @return the instance of this builder, for chaining calls.
         */
        public IntentBuilder addOperations(
                ContentProviderOperation... operations) {
            if (operations == null || operations.length == 0)
                return this;

            List<ContentProviderOperation> list = new ArrayList<ContentProviderOperation>();
            for (ContentProviderOperation operation : operations)
                list.add(operation);

            return addOperations(list);
        }// addOperations()

        /**
         * Adds operations to {@link CPOExecutor#EXTRA_OPERATIONS}.
         * 
         * @param operations
         *            operations.
         * @return the instance of this builder, for chaining calls.
         */
        public IntentBuilder addOperations(
                Collection<ContentProviderOperation> operations) {
            if (operations == null || operations.isEmpty())
                return this;

            @SuppressWarnings("unchecked")
            ArrayList<ContentProviderOperation> currentOperations = (ArrayList<ContentProviderOperation>) mResult
                    .getSerializableExtra(EXTRA_OPERATIONS);
            if (currentOperations == null)
                currentOperations = new ArrayList<ContentProviderOperation>();

            currentOperations.addAll(operations);
            mResult.putExtra(EXTRA_OPERATIONS, currentOperations);

            return this;
        }// addOperations()

        /**
         * Sets {@link CPOExecutor#EXTRA_OPERATIONS}.
         * 
         * @param operations
         *            operations.
         * @return the instance of this builder, for chaining calls.
         */
        public IntentBuilder setOperations(
                ContentProviderOperation... operations) {
            mResult.removeExtra(EXTRA_OPERATIONS);
            return addOperations(operations);
        }// setOperations()

        /**
         * Sets {@link CPOExecutor#EXTRA_OPERATIONS}.
         * 
         * @param operations
         *            operations.
         * @return the instance of this builder, for chaining calls.
         */
        public IntentBuilder setOperations(
                Collection<ContentProviderOperation> operations) {
            mResult.removeExtra(EXTRA_OPERATIONS);
            return addOperations(operations);
        }// setOperations()

        /**
         * Sets {@link CPOExecutor#EXTRA_RESULT_RECEIVER}.
         * 
         * @param resultReceiver
         *            result receiver.
         * @return the instance of this builder, for chaining calls.
         */
        public IntentBuilder setResultReceiver(ResultReceiver resultReceiver) {
            mResult.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver);
            return this;
        }// setResultReceiver()

        /**
         * Sets {@link CPOExecutor#EXTRA_PENDING_INTENT_RESULT}.
         * 
         * @param pendingIntent
         *            the pending intent.
         * @return the instance of this builder, for chaining calls.
         */
        public IntentBuilder setPendingIntentResult(PendingIntent pendingIntent) {
            mResult.putExtra(EXTRA_PENDING_INTENT_RESULT, pendingIntent);
            return this;
        }// setPendingIntentResult()

    }// IntentBuilder

    /**
     * Starts this service to execute given operations.
     *
     * @param context
     *            the context.
     * @param sessionId
     *            see {@link CPOExecutor#EXTRA_SESSION_ID}.
     * @param authority
     *            see {@link CPOExecutor#EXTRA_AUTHORITY}.
     * @param operations
     *            see {@link CPOExecutor#EXTRA_OPERATIONS}.
     * @return {@code true} if given extras were valid, and the call to start
     *         the service has been made.
     * @throws NullPointerException
     *             if given context is {@code null}.
     */
    public static boolean start(Context context, int sessionId,
            String authority, ContentProviderOperation... operations) {
        try {
            context.startService(new IntentBuilder(context)
                    .setSessionId(sessionId).setAuthority(authority)
                    .setOperations(operations).build());
            return true;
        } catch (IllegalArgumentException e) {
            if (DEBUG)
                Log.e(CLASSNAME, e.getMessage(), e);
            return false;
        }
    }// start()

    /**
     * Starts this service to execute given operations.
     *
     * @param context
     *            the context.
     * @param sessionId
     *            see {@link CPOExecutor#EXTRA_SESSION_ID}.
     * @param authority
     *            see {@link CPOExecutor#EXTRA_AUTHORITY}.
     * @param operations
     *            see {@link CPOExecutor#EXTRA_OPERATIONS}.
     * @return {@code true} if given extras were valid, and the call to start
     *         the service has been made.
     * @throws NullPointerException
     *             if given context is {@code null}.
     */
    public static boolean start(Context context, int sessionId,
            String authority, Collection<ContentProviderOperation> operations) {
        try {
            context.startService(new IntentBuilder(context)
                    .setSessionId(sessionId).setAuthority(authority)
                    .setOperations(operations).build());
            return true;
        } catch (IllegalArgumentException e) {
            if (DEBUG)
                Log.e(CLASSNAME, e.getMessage(), e);
            return false;
        }
    }// start()

    @Override
    protected void onHandleIntent(Intent intent) {
        if (DEBUG)
            Log.d(CLASSNAME, "onHandleIntent() >> " + intent);

        if (intent == null)
            return;

        if (ACTION_EXECUTE_OPERATIONS.equals(intent.getAction())) {
            executeOperations(intent);
        }// ACTION_EXECUTE_OPERATIONS
    }// onHandleIntent()

    /**
     * Executes given operations.
     * 
     * @param intent
     *            the original command intent from client.
     */
    protected void executeOperations(Intent intent) {
        final int sessionId = intent.getIntExtra(EXTRA_SESSION_ID, -1);
        if (sessionId < 0)
            return;

        final String authority = intent.getStringExtra(EXTRA_AUTHORITY);
        if (TextUtils.isEmpty(authority))
            return;

        @SuppressWarnings("unchecked")
        final ArrayList<ContentProviderOperation> operations = (ArrayList<ContentProviderOperation>) intent
                .getSerializableExtra(EXTRA_OPERATIONS);
        if (operations == null || operations.isEmpty())
            return;

        /*
         * Now execute the operations.
         */
        ContentProviderResult[] results = null;
        Throwable throwable = null;
        try {
            results = getContentResolver().applyBatch(authority, operations);
        } catch (Throwable t) {
            if (DEBUG)
                Log.e(CLASSNAME, t.getMessage(), t);
            throwable = t;
        }

        /*
         * Send result to result receiver.
         */
        ResultReceiver resultReceiver = intent
                .getParcelableExtra(EXTRA_RESULT_RECEIVER);
        if (resultReceiver != null) {
            Bundle result = new Bundle();
            result.putInt(EXTRA_SESSION_ID, sessionId);
            result.putParcelableArray(EXTRA_CONTENT_PROVIDER_RESULTS, results);
            result.putSerializable(EXTRA_THROWABLE, throwable);

            resultReceiver.send(throwable == null ? RESULT_OK : RESULT_ERROR,
                    result);
        }// if

        /*
         * Send result to pending intent.
         */
        PendingIntent pendingIntentResult = intent
                .getParcelableExtra(EXTRA_PENDING_INTENT_RESULT);
        if (pendingIntentResult != null) {
            Intent result = new Intent();
            result.putExtra(EXTRA_SESSION_ID, sessionId);
            result.putExtra(EXTRA_CONTENT_PROVIDER_RESULTS, results);
            result.putExtra(EXTRA_THROWABLE, throwable);

            try {
                pendingIntentResult.send(this, throwable == null ? RESULT_OK
                        : RESULT_ERROR, result);
            } catch (Throwable t) {
                if (DEBUG)
                    Log.e(CLASSNAME, t.getMessage(), t);
            }
        }// if
    }// executeOperations()

}
